/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.lucene.spatial.tier.projection;

import org.apache.lucene.spatial.geometry.DistanceUnits;

/**
 * CartesianTierPlotter provides functionality for plotting a point defined by a
 * longitude and latitude, in a box thats part of a cartesian tier.
 */
public class CartesianTierPlotter {

	private static final double LOG_2 = Math.log(2);

	private final int tierLevel;
	private final int tierLength;
	private final int tierVerticalPosDivider;
	private final Projector projector;

	/**
	 * Creates a new CartesianTierPlotter that will plot points at the given
	 * tier level using the given Projector
	 * 
	 * @param tierLevel
	 *            Tier Level the plotter should plot its points at
	 * @param projector
	 *            Projector that the plotter should use to project points onto a
	 *            tier
	 */
	public CartesianTierPlotter(int tierLevel, Projector projector) {
		this.tierLevel = tierLevel;
		this.projector = projector;

		this.tierLength = (int) Math.pow(2, this.tierLevel);
		this.tierVerticalPosDivider = calculateTierVerticalPosDivider();
	}

	public CartesianTierPlotter(double radius, Projector projector) {
		this(CartesianTierPlotter.bestFit(radius), projector);
	}

	/**
	 * Find the tier with the best fit for a bounding box. Best fit is defined
	 * as the ceiling of log2 (circumference of earth / distance) distance is
	 * defined as the smallest box fitting the corner between a radius and a
	 * bounding box.
	 * <p/>
	 * Distances less than a mile return 15, finer granularity is in accurate
	 * 
	 * @param miles
	 *            Distance in mile;s of the bounding box that the tier should be
	 *            a best fit for
	 * @return Tier level with the best fit for the bounding box
	 */

	static public int bestFit(double range) {
		return bestFit(range, DistanceUnits.MILES);
	}

	static public int bestFit(double range, DistanceUnits distanceUnit) {
		double times = distanceUnit.earthCircumference() / (2.0d * range);

		int bestFit = (int) Math.ceil(log2(times));

		if (bestFit > 15) {
			// 15 is the granularity of about 1 mile
			// finer granularity isn't accurate with standard java math
			return 15;
		}
		return bestFit;
	}

	/**
	 * TierBoxId is latitude box id + longitude box id where latitude box id,
	 * and longitude box id are transposded in to position coordinates.
	 * 
	 * @param latitude
	 *            Latitude of the point whose box id is to be returned
	 * @param longitude
	 *            Longitude of the point whose box id is to be returned
	 * @return Id of the box where the point with the latitude and longitude, is
	 *         located
	 */
	public double getTierBoxId(double latitude, double longitude) {
		double[] coords = projector.coords(latitude, longitude);
		double[] ranges = projector.range();

		double id = getBoxCoord(coords[0], ranges[0])
				+ (getBoxCoord(coords[1], ranges[1]) / tierVerticalPosDivider);

		return id;
	}

	// ================================================= Helper Methods
	// ================================================

	/**
	 * Calculates the nearest max power of 10 greater than the tierlen
	 * <p/>
	 * e.g tierId of 13 has tierLen 8192 nearest max power of 10 greater than
	 * tierLen would be 10,000
	 * 
	 * @return Nearest max power of 10 greater than the tier len
	 */
	private int calculateTierVerticalPosDivider() {
		return (int) Math.pow(10, (int) Math.ceil(Math.log10(this.tierLength)));
	}

	/**
	 * Returns the box id of the given coordinate
	 * 
	 * @param coord
	 *            Coordinate whose box id is to be computed
	 * @return Box id of the coordinate
	 */
	private double getBoxCoord(double coord, double range) {
		if (coord * 2 == range) {
			return (tierLength / 2) - 1; // Last box is border inclusive
		} else {
			return Math.floor(coord * (tierLength / range));
		}
	}

	/**
	 * Computes log to base 2 of the given value
	 * 
	 * @param value
	 *            Value to compute the log of
	 * @return Log_2 of the value
	 */
	public static double log2(double value) {
		return Math.log(value) / LOG_2;
	}

	// ================================================= Getters / Setters
	// =============================================

	/**
	 * Returns the nearest max power of 10 greater than the tier len
	 * 
	 * @return Nearest max power of 10 greater than the tier len
	 */
	public double getTierVerticalPosDivider() {
		return tierVerticalPosDivider;
	}

	/**
	 * Returns the ID of the tier level plotting is occuring at
	 * 
	 * @return ID of the tier level plotting is occuring at
	 */
	public int getTierLevelId() {
		return this.tierLevel;
	}
}
